package de.dailab.graphworld.world;

import java.awt.Color;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;

import de.dailab.graphworld.gui.GraphWorldGUI;
import de.dailab.graphworld.ontology.Edge;
import de.dailab.graphworld.ontology.Graph;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.Team;
import de.dailab.graphworld.ontology.exceptions.CapacityExceededException;
import de.dailab.graphworld.ontology.exceptions.ContestEndedException;
import de.dailab.graphworld.ontology.exceptions.ContestNotStartedException;
import de.dailab.graphworld.ontology.exceptions.GraphWorldException;
import de.dailab.graphworld.ontology.exceptions.InvalidPositionException;
import de.dailab.graphworld.ontology.exceptions.MoveException;
import de.dailab.graphworld.ontology.exceptions.MoveIntervalException;
import de.dailab.graphworld.ontology.exceptions.NoGoldCarriedException;
import de.dailab.jiactng.agentcore.AbstractAgentBean;
import de.dailab.jiactng.agentcore.IAgentNode;
import de.dailab.jiactng.agentcore.SimpleAgentNode;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.DoAction;
import de.dailab.jiactng.agentcore.action.scope.ActionScope;
import de.dailab.jiactng.agentcore.environment.IEffector;
import de.dailab.jiactng.agentcore.lifecycle.LifecycleException;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import de.dailab.jiactng.agentcore.ontology.IAgentDescription;

public class ExtendedGraphWorldAgentBean extends AbstractAgentBean implements IEffector {

	// action names
	/** Move an agent. Parameters: Target Position, Returns: new Position */
	public static final String ACTION_MOVE = "GraphWorld.Move";
	/** Take an amount of gold. Parameters: Amount of gold to take (Integer), Returns: Amount of Gold taken from Node*/
	public static final String ACTION_TAKE_GOLD = "GraphWorld.TakeGold";
	/** Drop all carried gold. Parameters: - */
	public static final String ACTION_DROP_GOLD = "GraphWorld.DropGold";
	/** Explore the world around the agent. Parameters: -, Returns: Collection of graph nodes */
	public static final String ACTION_EXPLORE = "GraphWorld.Explore";
	/** Receive a start position for an agent. Returns: Start Position */
	public static final String ACTION_INIT_POSITION = "GraphWorld.GetInitialPosition";
	/** Receive a start position for an agent and assign the agent to a team. Parameters: Team name, Returns: Start Position */
	public static final String ACTION_INIT_TEAM_POSITION = "GraphWorld.GetInitialTeamPosition";
	/** Drop a "home" pheromone on an edge . Parameters: Edge target position, Returns: Updated graph node */
	public static final String ACTION_DROP_HOME_PHEROMONE = "GraphWorld.DropHomePheromone";
	/** Drop a "gold" pheromone. Parameters: Edge target position, Returns: Updated graph node */
	public static final String ACTION_DROP_GOLD_PHEROMONE = "GraphWorld.DropGoldPheromone";
	private final Random random = new Random(System.identityHashCode(this));

	private static final String DEFAULTTEAMNAME = "noTeamName";
	
	// used by move action
	private int allowedMoveInterval = 1000;
	private Map<String, Long> agentsLastMove = new HashMap<String, Long>();
	
	// mapping to track which agent carries how much gold
	private Map<String, Integer> carryingGold = new HashMap<String, Integer>();
	
	// for team play, Map AgentID -> Team Name
	private Map<String, Team> teams = new HashMap<String, Team>();
	private Map<String, Team> teamMembers = new HashMap<String, Team>();
	
	//gui
	private GraphWorldGUI gui;
	
	//set by XML file
	//graph size
	private int size;
	//view range -> explore
	private int viewrange = 0;
	//max agents per node
	private int agentsPerNode = 1;
	//maximum amount of gold per node
	private int maxGoldPerNode = 1;
	//start at home position or at randomPosition
	private boolean startAtHome = false; 
	//decay factor for pheromones, how fast pheromones will dissolve
	private double pheromoneDecay = 0.9;
	//the amount of gold pieces an agent can carry at most
	private int agentGoldCapacity = 1;
	//total amount of gold per field, for contests
	private int totalGold = 99;
	
	private boolean contest = false;
	private long contestTimeLimit = 600000l;
	// for contests only
	private boolean started = false;
	private boolean ended = false;
	private long startTime = 0l;
	
	@Override
	public void doInit() {
		// initialize new graph and write him in memory
		Graph graph = new Graph();
		if (size == 49) {
			graph = Graph.createContestGraph(new Random(), totalGold, maxGoldPerNode, pheromoneDecay);
			this.memory.write(graph);
		} else {
			graph = new Graph(new Random(),size, maxGoldPerNode, pheromoneDecay);
			this.memory.write(graph);
		}
		// add all nodes to memory too
		for (final Node node : (ArrayList<Node>)graph.getNodes()) {
			this.memory.write(node);
		}
	}

	@Override
	public void doStart() {
		// create gui
		this.gui = new GraphWorldGUI(false);
		// load created graph to gui
		this.gui.setGraph(this.memory.read(new Graph(new Random(),size)));
		// window close options
		this.gui.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				super.windowClosed(e);
				IAgentNode node = null;
				if ((node = ExtendedGraphWorldAgentBean.this.thisAgent.getAgentNode()) instanceof SimpleAgentNode) {
					try {
						((SimpleAgentNode) node).shutdown();
					} catch (LifecycleException lex) {
						System.err.println("could not shut down this agent node: "+ lex.getMessage());
					}
				}
			}
		});
		// set visible
		this.gui.setVisible(true);
		// make it look nice
		this.refreshGUI();
	}

	@Override
	public void doStop() {
		// shut down everything
		if (this.gui != null) {
			this.gui.setVisible(false);
			this.gui.dispose();
			this.gui = null;
		}
	}

	private void refreshGUI() {
		// if gui exists
		if (this.gui != null) {
			// load actual graph from memory
			final Graph graph = this.memory.read(new Graph(new Random(),size));
			if (graph != null) {
				// refresh gui
				this.gui.update();
			} else {
				System.err.println("could not find gui");
			}
		}
	}

	private void initLocation(final DoAction action) {
		// create a template used for reading from agent's memory
		final Node tpl = new Node(null);
		tpl.agents.add(action.getSession().getOriginalAgentDescription());
		Node position = memory.read(tpl);

		if (log.isDebugEnabled()) {
			log.debug("agent (" + ((IAgentDescription) tpl.agents.get(0)).getAid()+ ") position from memory = " + position);
		}

		/*
		 * if a position was found where the agent is located, return this location 
		 * otherwise search for free and collision free position and assign the agent to this position
		 */
		if (position == null) {
			if (this.startAtHome) {
				position = this.memory.read(new Node(0));
			} else {
				do {
					final int id = this.random.nextInt(size);
					// read a node with random ID from graph
					position = this.memory.read(new Node(id));
				}
				// do the loop, until a free location was found
				while (position.getGold() == 0 && (position.agents.size()>=agentsPerNode));
				// mark the location as occupied
			}
			position.agents.add(action.getSession().getOriginalAgentDescription());
		}
		if (log.isDebugEnabled()) {
			log.debug("assigned agent position = " + position);
		}
		// report the location of marked location
		this.returnResult(action, new Serializable[] { position.createSubGraphCopy(1) });
	}
	
	/**
	 * Returns the color the next new team gets.
	 * @return AWT color
	 */
	private Color getNextTeamColor() {
		int numberOfTeams = teams.size();
		switch (numberOfTeams) {
		case 0: return Color.RED;
		case 1: return Color.GREEN;
		case 2: return Color.CYAN;
		case 3: return Color.ORANGE;
		default: return Color.RED;
		}
	}
	
	/**
	 * Initializes team location and adds the agent to the specified team.
	 * @param action
	 */
	private void initTeamLocation(final DoAction action) {
		final String teamName = (String)action.getParams()[0];
		log.info("Initializing new Agent for team "+teamName);
		Team team = teams.get(teamName);
		if (team == null) {
			team = new Team(teamName, this.getNextTeamColor());
			teams.put(teamName, team);
			gui.teamAdded(teamName);
		}
		teamMembers.put(action.getSession().getOriginalAgentDescription().getAid(), team);
		if (gui.getTeams() == null) {
			gui.setTeams(teamMembers);
		}
		
		if ( contest && (!started && teams.size() > 1)) {
			log.info("2 TEAMS REGISTERED! CONTEST STARTS!");
			started = true;
			startTime = System.currentTimeMillis();
		}
		
		initLocation(action);
	}

	private boolean exists(final Node n) {
		return (n.getID()<size && n.getID()>-1);
	}

	private Node read(final Node direction) {
		final Node changed = new Node(direction.getID());
		return this.memory.read(changed);
	}
	
	/**
	 * Checks if an agent is on the specified node.
	 * @param node graph node
	 * @param agent agent description
	 * @return true if the agent is currently located on this node
	 */
	private boolean isAgentAtPosition(final Node node, final IAgentDescription agent) {
		Node graphNode = this.read(node);
		return graphNode.agents.contains(agent);
	}
	
	private Node getAgentPosition(final IAgentDescription agent) {
		ArrayList<IAgentDescription> agents = new ArrayList<IAgentDescription>();
		agents.add(agent);
		Node template = new Node(null, null, null, null, agents);
		return this.memory.read(template);
	}

	// explores locations depenent on position and viewrange
	private void explore(final DoAction action) {

		try {
			// get location out of action and read it from memory
			final Node position = this.getAgentPosition(action.getSession().getOriginalAgentDescription());
			final Node tpl = new Node(position.getID());
			final Node graphPosition = this.memory.read(tpl);

			if (graphPosition == null) {
				this.returnFailure(action, new InvalidPositionException(position));
				return;
			}
			
			final HashSet<Node> result = new HashSet<Node>();
			
			// viewrange only local (0)
			if(viewrange==0){
				result.add(graphPosition.createSubGraphCopy(1));
				
			// viewrange reaches one neighbor (1)
			}else if(viewrange==1){
				// TODO implementation required
				log.warn("Implementation for view range 1 required!");
				
			// viewrange reaches all neighbors (2)
			}else if(viewrange==2){
				result.add(graphPosition.createSubGraphCopy(1));
				for (Edge e : (ArrayList<Edge>)graphPosition.getEdges()) {
					if (this.exists(e.getTarget())) {
						Node pos = this.read(e.getTarget());
						if (pos == null) {
							log.warn("Position read from memory is null; original position: "+ position + ", direction: " + e);
						}
						result.add(pos.createSubGraphCopy(1));
					}
				}
			}else{
				this.returnFailure(action, new GraphWorldException("viewrange not set properly", position));
				return;
			}
			this.returnResult(action, new Serializable[] { result });
		} catch (final Exception e) {
			this.returnFailure(action, e);
		}

	}

	// updates agent movement
	private void move(final DoAction action) {
		try {
			// get movement nodes from action
			final Node position = this.getAgentPosition(action.getSession().getOriginalAgentDescription());
			final Node direction = (Node) action.getParams()[0];

			// get moving agent from action
			IAgentDescription movingAgent = action.getSession().getOriginalAgentDescription();

			// check for illegal move interval
			Long last = agentsLastMove.get(movingAgent.getAid());
			if (last == null) {
				agentsLastMove.put(movingAgent.getAid(),System.currentTimeMillis());
			} else {
				if ((last + allowedMoveInterval) > System.currentTimeMillis()) {
					this.returnFailure(action, new MoveIntervalException(allowedMoveInterval, position));
					return;
				} else {
					agentsLastMove.put(movingAgent.getAid(),System.currentTimeMillis());
				}
			}

			if (this.exists(direction)) {
				Node move = this.memory.read(new Node(direction.getID()));
				// check if the agent is actually on the start position he sent (exploit prevention)
				final Node clear = this.memory.read(new Node(position.getID()));
				if (this.isAgentAtPosition(clear, action.getSession().getOriginalAgentDescription())) {
					// check for collisions
					if (move.agents.size()>=agentsPerNode) {
						this.returnFailure(action, new MoveException(MoveException.Problem.BLOCKED, move));
					} else if (clear.isNeighbor(direction)) {
						// update location of the moving agent
						move.agents.add(action.getSession().getOriginalAgentDescription());
						// clear old position of the requesting agent
						
						clear.agents.remove(action.getSession().getOriginalAgentDescription());
						memory.update(clear, clear);
						// return the current location of the agent
						this.returnResult(action, new Serializable[] { move.createSubGraphCopy(1) });
					} else {
						this.returnFailure(action, new MoveException(MoveException.Problem.TARGET_NOT_NEIGHBOUR, new Node(direction.getID())));
					}
				} else {
					this.returnFailure(action, new InvalidPositionException(new Node(position.getID())));
				}
			} else {
				final GraphWorldException e = new MoveException(MoveException.Problem.TARGET_INVALID, new Node(direction.getID()));
				this.returnFailure(action, e);
			}
		} catch (final Exception e) {
			this.returnFailure(action, e);
		}
	}

	// agent collects gold
	private void takeGold(final DoAction origin) {
		try {
			// get amount to take from parameters
			final Integer amountToTake = (Integer) origin.getParams()[0];
			//get node from memory
			final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
			
			int capacity = this.agentGoldCapacity;
			String agentId = origin.getSession().getOriginalAgentDescription().getAid();
		
			if (amountToTake < 0) {
				this.returnFailure(origin, new GraphWorldException("Negative values are not allowed!", position));
			}
			
			if (carryingGold.containsKey(agentId)) {
				capacity -= carryingGold.get(agentId).intValue();	
			}
			
			if (amountToTake > capacity) {
				this.returnFailure(origin, new CapacityExceededException(this.agentGoldCapacity, position));
				return;
			}
			
			
			
			if (this.exists(position)) {
				final Node read = this.memory.read(new Node(position.getID()));
				// update gold
				if (read.getGold()>0) {
					int taken = amountToTake;
					if (read.getGold() < amountToTake) {
						taken = read.getGold();
						read.setGold(0);
					} else {
						read.setGold(read.getGold()-amountToTake);
					}
					
					if (carryingGold.containsKey(agentId)) {
						carryingGold.put(agentId, Integer.valueOf(carryingGold.get(agentId).intValue() + taken));
					} else {
						carryingGold.put(agentId, Integer.valueOf(taken));
					}
					this.returnResult(origin, new Serializable[] {Integer.valueOf(taken)});
				} else {
					this.returnResult(origin, new Serializable[] {Integer.valueOf(0)});
				}
			} else {
				final GraphWorldException e = new InvalidPositionException(position);
				this.returnFailure(origin, e);
			}
		} catch (final Exception e) {
			this.returnFailure(origin, e);
		}
	}
	
	// agent drops gold
	private void dropGold(final DoAction origin) {

		try {
			// get position from action
			final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
			IAgentDescription agent = origin.getSession().getOriginalAgentDescription();
			
			if (!carryingGold.containsKey(agent.getAid())) {
				this.returnFailure(origin, new NoGoldCarriedException(new Node(position.getID())));
				return;
			}
	
			if (this.exists(position)) {
				final Node read = this.memory.read(new Node(position.getID()));
				if (read.getID() == 0) {
					// this is a drop at HOME location
					if (agent != null) {
						int carried = carryingGold.remove(agent.getAid()).intValue();
						gui.collected += carried;
						Team team = this.teamMembers.get(agent.getAid());
						if (team != null) {
							gui.increaseTeamScore(team.getTeamName(), carried);
						}
//						refreshGUI();
					}
					this.returnResult(origin, new Serializable[] {});
				} else {
					// drop on non-home location => just increase gold on node by 1
					
					int carried = carryingGold.remove(agent.getAid()).intValue();
					read.setGold(read.getGold()+carried);
				}
			} else {
				final GraphWorldException e = new InvalidPositionException(position);
				this.returnFailure(origin, e);
			}
		} catch (final Exception e) {
			this.returnFailure(origin, e);
		}
	}

	@Override
	public void doAction(final DoAction doAction) throws Exception {

		final String aname = doAction.getAction().getName();

		if (ExtendedGraphWorldAgentBean.ACTION_INIT_TEAM_POSITION.equals(aname)) {this.initTeamLocation(doAction); }
		else if (ExtendedGraphWorldAgentBean.ACTION_INIT_POSITION.equals(aname)) {this.initLocation(doAction); }
		else if ((started == contest) && (!ended)) {
			if (ExtendedGraphWorldAgentBean.ACTION_MOVE.equals(aname)) {this.move(doAction);
			} else if (ExtendedGraphWorldAgentBean.ACTION_EXPLORE.equals(aname)) {this.explore(doAction);
			} else if (ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD.equals(aname)) {this.takeGold(doAction);
			} else if (ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD.equals(aname)) {this.dropGold(doAction);
			} else if (ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD_PHEROMONE.equals(aname)) {this.dropGoldPheromone(doAction);
			} else if (ExtendedGraphWorldAgentBean.ACTION_DROP_HOME_PHEROMONE.equals(aname)) {this.dropHomePheromone(doAction);
			}
		} else if (!started){
			this.returnFailure(doAction, new ContestNotStartedException());
		} else if (ended) {
			this.returnFailure(doAction, new ContestEndedException());
		}
		
		
//		this.refreshGUI();
	}
	
	// drop of pheromones => ONLY USED BY SWARM AGENT
	private void dropHomePheromone(final DoAction origin) {
		String teamName = DEFAULTTEAMNAME;
		Team team = this.teamMembers.get(origin.getSession().getOriginalAgentDescription().getAid());
		if (team != null) {
			teamName = team.getTeamName();
		}
		try {
			// get position and target from action
			final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
			final Node target = (Node) origin.getParams()[0];
			if (this.exists(position) && this.exists(target)) {
				
				final Node src = this.memory.read(new Node(position.getID()));
				// look for correct edge to drop on
				for(Edge e : src.getEdges()){
					if(e.getTarget().getID() == target.getID()){
						e.dropHomePheromone(teamName);
						break;
					}
				}
				this.returnResult(origin, new Serializable[] {src.createSubGraphCopy(1)});
			} else {
				if(!this.exists(position)){
					final GraphWorldException e = new InvalidPositionException(position);
					this.returnFailure(origin, e);
				}else{
					final GraphWorldException e = new InvalidPositionException(target);
					this.returnFailure(origin, e);
				}
			}
		} catch (final Exception e) {
			this.returnFailure(origin, e);
		}
		
	}
	
	// see dropHomePheromone
	private void dropGoldPheromone(final DoAction origin) {
		String teamName = DEFAULTTEAMNAME;
		Team team = this.teamMembers.get(origin.getSession().getOriginalAgentDescription().getAid());
		if (team != null) {
			teamName = team.getTeamName();
		}
		try {
			final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
			final Node target = (Node) origin.getParams()[0];
			if (this.exists(position) && this.exists(target)) {
				if (!this.isAgentAtPosition(position, origin.getSession().getOriginalAgentDescription())) {
					this.returnFailure(origin, new GraphWorldException("Agent is not on this position!", new Node(position.getID())));
					return;
				}
				final Node src = this.memory.read(new Node(position.getID()));
				for(Edge e : src.getEdges()){
					if(e.getTarget().getID() == target.getID()){
						e.dropGoldPheromone(teamName);
						break;
					}
				}
				this.returnResult(origin, new Serializable[] {src.createSubGraphCopy(1)});
			} else {
				if(!this.exists(position)){
					final GraphWorldException e = new InvalidPositionException(position);
					this.returnFailure(origin, e);
				}else{
					final GraphWorldException e = new InvalidPositionException(target);
					this.returnFailure(origin, e);
				}
			}
		} catch (final Exception e) {
			this.returnFailure(origin, e);
		}
	}

	@Override
	public List<? extends IActionDescription> getActions() {
		List<Action> actions = new ArrayList<Action>();

		actions.add(new Action(ACTION_INIT_POSITION, this, new Class[] {},new Class[] { Node.class }));
		actions.add(new Action(ACTION_INIT_TEAM_POSITION, this, new Class[] {String.class}, new Class[] { Node.class} ));
		actions.add(new Action(ACTION_EXPLORE, this,new Class[] { },new Class[] { Collection.class }));
		actions.add(new Action(ACTION_MOVE, this, new Class[] { Node.class }, new Class[] { Node.class }));
		actions.add(new Action(ACTION_TAKE_GOLD, this,new Class[] { Integer.class }, new Class[] { Integer.class }));
		actions.add(new Action(ACTION_DROP_GOLD, this,new Class[] { }, new Class[] {}));
		actions.add(new Action(ACTION_DROP_GOLD_PHEROMONE, this, new Class[] {Node.class}, new Class[] {}));
		actions.add(new Action(ACTION_DROP_HOME_PHEROMONE, this, new Class[] {Node.class}, new Class[] {}));	

		for (Action a : actions) {
			a.setScope(ActionScope.GLOBAL);
		}

		return actions;
	}

	public int getSize() {
		return size;
	}

	public void setSize(int size) {
		this.size = size;
	}

	public int getViewrange() {
		return viewrange;
	}

	public void setViewrange(int viewrange) {
		this.viewrange = viewrange;
	}
	
	public int getAgentsPerNode() {
		return agentsPerNode;
	}

	public void setAgentsPerNode(int agentsPerNode) {
		this.agentsPerNode = agentsPerNode;
	}

	public int getMaxGoldPerNode() {
		return maxGoldPerNode;
	}

	public void setMaxGoldPerNode(int maxGoldPerNode) {
		this.maxGoldPerNode = maxGoldPerNode;
	}

	public boolean isStartAtHome() {
		return startAtHome;
	}

	public void setStartAtHome(boolean startAtHome) {
		this.startAtHome = startAtHome;
	}

	public double getPheromoneDecay() {
		return pheromoneDecay;
	}

	public void setPheromoneDecay(double pheromoneDecay) {
		this.pheromoneDecay = pheromoneDecay;
	}

	public int getAgentGoldCapacity() {
		return agentGoldCapacity;
	}

	public void setAgentGoldCapacity(int agentGoldCapacity) {
		this.agentGoldCapacity = agentGoldCapacity;
	}

	public int getTotalGold() {
		return totalGold;
	}

	public void setTotalGold(int totalGold) {
		this.totalGold = totalGold;
	}

	public boolean isContest() {
		return contest;
	}

	public void setContest(boolean contest) {
		this.contest = contest;
	}

	public long getContestTimeLimit() {
		return contestTimeLimit;
	}

	public void setContestTimeLimit(long contestTimeLimit) {
		this.contestTimeLimit = contestTimeLimit;
	}

	@Override
	public void execute() {
		if (contest && started && !ended)  {
			long timePlayed = System.currentTimeMillis() - startTime;
			Graph g = this.memory.read(new Graph(new Random(),size));
			int goldRemaining = g.getAllGold();
			if (timePlayed >= contestTimeLimit || goldRemaining <= 0) {
				ended = true;
			}
			this.gui.setPlayedTime(timePlayed);
		}
		
		this.refreshGUI();
	}	
	
	
}
